perf: drop explicit zeros from the constraint matrix (#814)#815
Merged
Conversation
Expressions that broadcast against a dense coordinate store one coefficient per pair, most of them structurally zero. Those explicit zeros were carried all the way into `matrices.A` and thus into every solver handoff. `highspy.Highs.addRows` (and the other direct backends' matrix loaders) scale with *stored* nnz, so the handoff spent most of its work describing zeros — e.g. sparse_network(250) stored 1.5M entries for 18k structural nonzeros (98.8% zeros). Prune them once, centrally, in `_stack` via `eliminate_zeros()`, so `A` and `indicator_A` — and hence HiGHS, gurobi, xpress, copt, mosek and the LP/MPS writers — all hand the solver only structural nonzeros. A zero coefficient never changes a constraint, so this is mathematically identical. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Merging this PR will improve performance by 29.51%
Performance Changes
Tip Curious why this is faster? Comment Comparing Footnotes
|
FabianHofmann
approved these changes
Jul 13, 2026
FabianHofmann
left a comment
Collaborator
There was a problem hiding this comment.
thanks @FBumann for detecting and fixing this!
FabianHofmann
added a commit
that referenced
this pull request
Jul 14, 2026
…#816) * perf: drop explicit zeros from the constraint matrix (#814) Expressions that broadcast against a dense coordinate store one coefficient per pair, most of them structurally zero. Those explicit zeros were carried all the way into `matrices.A` and thus into every solver handoff. `highspy.Highs.addRows` (and the other direct backends' matrix loaders) scale with *stored* nnz, so the handoff spent most of its work describing zeros — e.g. sparse_network(250) stored 1.5M entries for 18k structural nonzeros (98.8% zeros). Prune them once, centrally, in `_stack` via `eliminate_zeros()`, so `A` and `indicator_A` — and hence HiGHS, gurobi, xpress, copt, mosek and the LP/MPS writers — all hand the solver only structural nonzeros. A zero coefficient never changes a constraint, so this is mathematically identical. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * perf: prune explicit zeros at matrix assembly to cut build peak memory Follow-up to #815. That eliminates zeros from the stacked constraint matrix after scipy.vstack has already materialised the full dense-with-zeros block, so the peak allocation (what the CodSpeed memory instrument tracks) is unchanged — only the resting size and solver-ingest cost drop. Prune at the source instead: fold `coeffs != 0` into the valid-entry mask in Constraint._matrix_export_data, so broadcast zeros never enter cols/data/the CSR. Snapshot capture and the matrix build share this path, so they stay consistent. Two ripples handled: - matrices.dual derived active rows from stored nnz (np.diff(indptr)); an all-zero-coefficient row would lose its dual slot once pruned. Derive active rows from row activity via a new ConstraintBase.active_row_mask (also avoids rebuilding the CSR just to count rows). - A zero-coefficient term no longer changes the sparsity pattern, so the persistent warm-start path no longer forces a SPARSITY rebuild for it. Test updated to use a non-zero coefficient; a no-rebuild case added. sparse_network(250): build peak 50 -> 36 MB (~27%), identical solver result. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * ci: trigger CI Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Co-authored-by: Fabian Hofmann <fab.hof@gmx.de>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
This opportunity was discovered while exploring the regression introduced by highspy==1.15
Depeneding on the model, this is quite a impressive improvement. But id only merge it after someone tested it with real world models and maybe also hardened tests with other solvers.
Closes #814.
Note
The following was generated by AI.
What & why
Expressions that broadcast against a dense coordinate (e.g. a
bus × lineincidence block) store one coefficient per coordinate pair, most of them
structurally zero. Those explicit zeros were carried into
model.matrices.Aand handed to every solver.
highspy.Highs.addRows— and the other directbackends' matrix loaders — scale with stored nnz, so the handoff spent most
of its work describing zeros (
sparse_network(250): 1.5M stored entries for18k structural nonzeros, 98.8% zeros).
Change
Prune explicit zeros once, centrally, in
matrices._stackviaeliminate_zeros(). This covers bothAandindicator_A, so allconsumers benefit — HiGHS, gurobi, xpress, copt, mosek and the LP/MPS
writers — not just HiGHS. A zero coefficient never changes a constraint, so
the result is mathematically identical; row count (and thus
clabelsalignment) is preserved, and the
dual/solpaths recompute independently ofA.Chose the central
matriceslayer over the per-backend_build_solver_modelpatch from the issue precisely so every backend and writer shares the win.
Tests
test_matrices_drops_explicit_zerosasserts a dense-broadcast constraintyields only structural nonzeros (stored-vs-structural nnz regression).
test_matrices.pyandtest_solvers.pypass unchanged.Expected addRows speedup (from #813 standalone repro)
Side effect: makes linopy insensitive to the #813 highspy
addRowsregression.🤖 Generated with Claude Code